﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

#region Aliases

using Control = System.Windows.Forms.Control;
using Form = System.Windows.Forms.Form;
using FormClosingEventArgs = System.Windows.Forms.FormClosingEventArgs;
using FormClosingEventHandler = System.Windows.Forms.FormClosingEventHandler;
using SysPoint = System.Drawing.Point;

#endregion

namespace FxArchitect.Xna_3_1
{
    public class WpfGame : Game
    {
        #region Fields

        Viewport viewport = new Viewport()
        {
            X = 0, Y = 0, Width = 1024, Height = 768, 
            MinDepth = 0.01f, MaxDepth = 1000f
        };
        Color clearColor = Color.Black;

        GraphicsDeviceManager graphics;
        Form gameForm, targetForm;
        Control targetControl;
        bool targetFormActive, drawingOnTargetForm,
             activeFixedTimeStep, deactiveFixedTimeStep,
             resizingTargetForm, resetGraphicDevice;

        #endregion

        #region Properties

        /// <summary>
        /// The graphics device manager responsible for this game's graphics device.
        /// </summary>
        public GraphicsDeviceManager GraphicsDeviceManager { get { return graphics; } }

        /// <summary>
        /// The form in which the game will target with drawing instead of it's own, when this
        /// value is set the appropriate actions will be taken to migrate the drawing to the new
        /// form.
        /// </summary>
        public Form TargetForm
        {
            get { return targetForm; }
            set
            {
                if (targetForm != value)
                {
                    if (targetForm != null)
                        UnhookTargetFormEvents();

                    targetForm = value;
                    HookTargetFormEvents();
                }
            }
        }
        /// <summary>
        /// The control in which the game will target with drawing calls, when this value is set
        /// appropriate actions are taken to migrate tracking of the presentation area.
        /// </summary>
        public Control TargetControl
        {
            get { return targetControl; }
            set
            {
                if (targetControl != value)
                {
                    if (targetControl != null)
                        UnhookTargetControlEvents();

                    targetControl = value;

                    if (targetControl != null)
                        TargetForm = targetControl.FindForm();

                    HookTargetControlEvents();
                }
            }
        }

        /// <summary>
        /// Indicates whether the game should draw directly onto the target form.
        /// </summary>
        public bool IsDrawingOnTargetForm
        {
            get { return drawingOnTargetForm; }
            set
            {
                // If the game was drawing on the form but isn't anymore, need to
                //  invalidate the form's background to get it redrawn by gdi
                if (drawingOnTargetForm && !value)
                {
                    try { targetForm.Invalidate(false); }
                    catch { }
                    finally { drawingOnTargetForm = value; }
                }
                drawingOnTargetForm = value;
            }
        }

        /// <summary>
        /// Should this game use it's TargetElapsedTime to space out Update calls when it's target form
        /// is the focus of windows input.
        /// </summary>
        public bool IsActiveFixedTimeStep
        {
            get { return activeFixedTimeStep; }
            set
            {
                activeFixedTimeStep = value;

                if (IsActive)
                    IsFixedTimeStep = activeFixedTimeStep;
            }
        }

        /// <summary>
        /// Should this game use it's TargetElapsedTime to space out Update calls when it's target form
        /// is not the focus of windows input.
        /// </summary>
        public bool IsDeactiveFixedTimeStep
        {
            get { return deactiveFixedTimeStep; }
            set
            {
                deactiveFixedTimeStep = value;

                if (!IsActive)
                    IsFixedTimeStep = deactiveFixedTimeStep;
            }
        }

        /// <summary>
        /// Indicates wheather the game is currently the active application. Overriden to allow the target
        /// form to be factored into active checks.
        /// </summary>
        public new bool IsActive { get { return (targetForm != null) ? targetFormActive : base.IsActive; } }

        #endregion

        #region Initialization

        /// <summary>
        /// Create a new FormGame.
        /// </summary>
        public WpfGame()
            : this(null)
        {
        }
        /// <summary>
        /// Create a new FormGame.
        /// </summary>
        /// <param name="targetControl">A control which the game will draw onto.</param>
        public WpfGame(Control targetControl)
            : this((targetControl != null) ? targetControl.FindForm() : null)
        {
            TargetControl = targetControl;
        }
        /// <summary>
        /// Create a new FormGame.
        /// </summary>
        /// <param name="targetForm">A form which the game will track and optionally draw onto.</param>
        public WpfGame(Form targetForm)
            : base()
        {
            Content.RootDirectory = string.Empty;

            IsMouseVisible = true;

            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredBackBufferWidth = viewport.Width;
            graphics.PreferredBackBufferHeight = viewport.Height;
            graphics.PreferredDepthStencilFormat = DepthFormat.Depth24Stencil8;
            graphics.PreferMultiSampling = false;
            graphics.SynchronizeWithVerticalRetrace = false;
            graphics.PreparingDeviceSettings += new EventHandler<PreparingDeviceSettingsEventArgs>(OnGraphicsDevicePrepareSettings);

            gameForm = Control.FromHandle(Window.Handle) as Form;
            gameForm.Shown += new EventHandler(GameForm_Shown);
            gameForm.LocationChanged += new EventHandler(GameForm_LocationChanged);

            TargetForm = targetForm;

            deactiveFixedTimeStep = true;
        }

        /// <summary>
        /// If a target form exist show it when the game begins to run before the first update occurs.
        /// </summary>
        protected override void BeginRun()
        {
            base.BeginRun();
            if (targetForm != null)
                targetForm.Show();
        }

        #endregion

        #region Drawing

        /// <summary>
        /// Starts the drawing of a frame, overriden to allow control logic.
        /// </summary>
        /// <returns>True if it's ok to proceed with the draw loop, false if it isn't ok.</returns>
        protected override bool BeginDraw()
        {
            // before proceeding perform some of the easy escape clauses and ensure the viewport fits
            if (targetControl != null)
            {
                // can't draw since the target control isn't visible
                if (!targetControl.Visible) return false;

                // if the viewport doesn't match the client area of the target control, it'll need adjustments
                if (targetControl.ClientSize.Width != viewport.Width ||
                    targetControl.ClientSize.Height != viewport.Height)
                {
                    Viewport nvp = viewport;
                    nvp.Width = targetControl.ClientSize.Width;
                    nvp.Height = targetControl.ClientSize.Height;
                    viewport = nvp;
                }
            }
            else if (targetForm != null && drawingOnTargetForm)
            {
                // don't do any drawing if the target form isn't visible
                if (!targetForm.Visible) return false;

                // if the viewport doesn't match the client area of the target form, it'll need adjustments
                if (targetForm.ClientSize.Width != viewport.Width ||
                    targetForm.ClientSize.Height != viewport.Height)
                {
                    Viewport nvp = viewport;
                    nvp.Width = targetForm.ClientSize.Width;
                    nvp.Height = targetForm.ClientSize.Height;
                    viewport = nvp;
                }
            }

            // if the viewport no longer fits into the back buffer, atempt to resize it
            if (graphics.PreferredBackBufferWidth < viewport.Width ||
                graphics.PreferredBackBufferHeight < viewport.Height)
            {
                graphics.PreferredBackBufferWidth = viewport.Width;
                graphics.PreferredBackBufferHeight = viewport.Height;
                resetGraphicDevice = true;
            }

            // if the target controls have changed so much to resize the back buffer then
            //  apply the changes before any drawing takes place.
            if (resetGraphicDevice && !resizingTargetForm)
            {
                graphics.ApplyChanges();
                resetGraphicDevice = false;
            }

            // if it's ok to draw then do the basic preparations
            bool beginDraw = base.BeginDraw();
            if (beginDraw)
            {
                if (targetForm != null || targetControl != null)
                    Microsoft.Xna.Framework.Input.Mouse.WindowHandle = (targetControl ?? targetForm).Handle;

                GraphicsDevice.Viewport = viewport;
                GraphicsDevice.Clear(clearColor);
            }
            return beginDraw;
        }

        /// <summary>
        /// Presents the contents of the back buffer to the screen, overriden to allow controls
        /// and forms to be the target of present calls.
        /// </summary>
        protected override void EndDraw()
        {
            if (targetControl != null)
            {
                Rectangle source = new Rectangle();
                source.Width = targetControl.ClientSize.Width;
                source.Height = targetControl.ClientSize.Height;
                GraphicsDevice.Present(source, null, targetControl.Handle);
            }
            else if (targetForm != null && drawingOnTargetForm)
            {
                Rectangle source = new Rectangle();
                source.Width = targetForm.Width;
                source.Height = targetForm.Height;
                GraphicsDevice.Present(source, null, targetForm.Handle);
            }
            else
                base.EndDraw();
        }

        #endregion

        #region Event Management

        /// <summary>
        /// Hook the event handlers to the appropriate events so that we can monitor what's going on
        /// with the target form. When done try to force fire the move and resize end to allow the
        /// viewport and game window to resize and move to the right position.
        /// </summary>
        void HookTargetFormEvents()
        {
            if (targetForm != null)
            {
                targetForm.Move += new EventHandler(TargetForm_Move);
                targetForm.ClientSizeChanged += new EventHandler(TargetForm_ClientSizeChanged);
                targetForm.ResizeBegin += new EventHandler(TargetForm_ResizeBegin);
                targetForm.ResizeEnd += new EventHandler(TargetForm_ResizeEnd);
                targetForm.Activated += new EventHandler(TargetForm_Activated);
                targetForm.Deactivate += new EventHandler(TargetForm_Deactivate);
                targetForm.FormClosing += new FormClosingEventHandler(TargetForm_FormClosing);

                try
                {
                    TargetForm_Move(targetForm, EventArgs.Empty);
                    TargetForm_ClientSizeChanged(targetForm, EventArgs.Empty);
                }
                catch { /* If the form is suspended these can fail, but they'll fire automatically on resume */ }

                // If this isn't set, there are more than just problems with mouse input that creep up
                Microsoft.Xna.Framework.Input.Mouse.WindowHandle = targetForm.Handle;
            }
        }

        /// <summary>
        /// Unhook the events from the target form and move the mouse handle back to the game form, also 
        /// make the game form visible so that it can be closed later if no new form is attached.
        /// </summary>
        void UnhookTargetFormEvents()
        {
            if (targetForm != null)
            {
                targetForm.Move -= new EventHandler(TargetForm_Move);
                targetForm.ClientSizeChanged -= new EventHandler(TargetForm_ClientSizeChanged);
                targetForm.ResizeBegin -= new EventHandler(TargetForm_ResizeBegin);
                targetForm.ResizeEnd -= new EventHandler(TargetForm_ResizeEnd);
                targetForm.Activated -= new EventHandler(TargetForm_Activated);
                targetForm.Deactivate -= new EventHandler(TargetForm_Deactivate);
                targetForm.FormClosing -= new FormClosingEventHandler(TargetForm_FormClosing);
            }

            Microsoft.Xna.Framework.Input.Mouse.WindowHandle = Window.Handle;
            gameForm.Show();
        }

        /// <summary>
        /// Hook events of the target control to ensure the game has present information.
        /// </summary>
        void HookTargetControlEvents()
        {
            if (targetControl != null && targetControl != targetForm)
                targetControl.Disposed += new EventHandler(TargetControl_Disposed);
        }

        /// <summary>
        /// Unhook the events to the target control.
        /// </summary>
        void UnhookTargetControlEvents()
        {
            if (targetControl != null && targetControl != targetForm)
                targetControl.Disposed -= new EventHandler(TargetControl_Disposed);
        }

        #endregion

        #region TargetControl Event Handlers

        /// <summary>
        /// Use the functionality of the TargetControl property to get events unhooked and the reference
        /// to the control nulled out.
        /// </summary>
        void TargetControl_Disposed(object sender, EventArgs e) { TargetControl = null; }

        #endregion

        #region TargetForm Event Handlers

        /// <summary>
        /// When the form moves, we need to force the game form to follow it so that mouse locations
        /// will match up.
        /// </summary>
        void TargetForm_Move(object sender, EventArgs e)
        {
            // Get the locations of each form's client area in screen coordinates
            SysPoint gameFormScreenLocation = gameForm.PointToScreen(gameForm.ClientRectangle.Location);
            SysPoint targetFormScreenLocation = targetForm.PointToScreen(targetForm.ClientRectangle.Location);

            // The new location of the game form, compensating for varying window themes
            gameForm.Location = new SysPoint(
                targetFormScreenLocation.X - (gameFormScreenLocation.X - gameForm.Location.X),
                targetFormScreenLocation.Y - (gameFormScreenLocation.Y - gameForm.Location.Y));
        }

        /// <summary>
        /// Track when resizing begins so the graphics device doesn't get reset every tick of the resize.
        /// </summary>
        void TargetForm_ResizeBegin(object sender, EventArgs e)
        {
            resizingTargetForm = true;
        }
        /// <summary>
        /// When the form is resized, we need to resize the game form with it to allow the graphics
        /// device to resize the back buffer as needed along with update the viewport with the new size.
        /// </summary>
        void TargetForm_ClientSizeChanged(object sender, EventArgs e)
        {
            try
            {
                if (GraphicsDevice.PresentationParameters.BackBufferWidth < targetForm.ClientSize.Width)
                {
                    graphics.PreferredBackBufferWidth = targetForm.ClientSize.Width;
                    resetGraphicDevice = true;
                }
                if (GraphicsDevice.PresentationParameters.BackBufferHeight < targetForm.ClientSize.Height)
                {
                    graphics.PreferredBackBufferHeight = targetForm.ClientSize.Height;
                    resetGraphicDevice = true;
                }
            }
            finally { TargetForm_Move(sender, e); }
        }
        /// <summary>
        /// Once resizing ends, it's safe to reset the graphics device, note that this doesn't happen when
        /// maximizing and restoring the target form, so additional checking is required in the draw method.
        /// </summary>
        void TargetForm_ResizeEnd(object sender, EventArgs e)
        {
            resizingTargetForm = false;

            if (resetGraphicDevice)
            {
                graphics.ApplyChanges();
                resetGraphicDevice = false;
            }
        }

        /// <summary>
        /// Toggle the form active flag on when the form is activated.
        /// </summary>
        void TargetForm_Activated(object sender, EventArgs e)
        {
            targetFormActive = true;
            IsFixedTimeStep = IsActiveFixedTimeStep;
        }
        /// <summary>
        /// Toggle the form active flag off when teh form is deactivated.
        /// </summary>
        void TargetForm_Deactivate(object sender, EventArgs e)
        {
            targetFormActive = false;
            IsFixedTimeStep = IsDeactiveFixedTimeStep;
        }

        /// <summary>
        /// When the form closes, the game should close as well.
        /// </summary>
        void TargetForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (!e.Cancel) Exit();
        }

        #endregion

        #region GameForm Event Handlers

        /// <summary>
        /// When a graphics device is being created, we need to ensure swap effect copy is enabled
        /// so that the contents of the back buffer are not lost after multiple present calls.
        /// </summary>
        protected virtual void OnGraphicsDevicePrepareSettings(object sender, PreparingDeviceSettingsEventArgs e)
        {
            e.GraphicsDeviceInformation.PresentationParameters.SwapEffect = SwapEffect.Copy;
        }

        /// <summary>
        /// When the Game's form is shown, hide it since we'll be using our own.
        /// </summary>
        void GameForm_Shown(object sender, EventArgs e)
        {
            if (targetForm != null || targetControl != null)
                gameForm.Hide();
        }

        /// <summary>
        /// Sometimes the game form gets the wrong positions during target form moves, we need to check
        /// for that case and compensate.
        /// </summary>
        void GameForm_LocationChanged(object sender, EventArgs e)
        {
            // Just pass off to the target form move handler if there is a target form
            if (targetForm != null)
                TargetForm_Move(sender, e);
        }

        #endregion
    }
}
